Подробен анализ на разпространението на асинхронен контекст в JavaScript с AsyncLocalStorage, с фокус върху проследяване на заявки, продължение и практически приложения за изграждане на стабилни и наблюдаеми сървърни приложения.
Разпространение на асинхронен контекст в JavaScript: Проследяване на заявки и продължение с AsyncLocalStorage
В съвременната сървърна разработка на JavaScript, особено с Node.js, асинхронните операции са повсеместни. Управлението на състояние и контекст през тези асинхронни граници може да бъде предизвикателство. Тази статия разглежда концепцията за разпространение на асинхронен контекст, като се фокусира върху това как да се използва AsyncLocalStorage за ефективно постигане на проследяване на заявки и продължение. Ще разгледаме неговите предимства, ограничения и реални приложения, като предоставим практически примери, за да илюстрираме употребата му.
Разбиране на разпространението на асинхронен контекст
Разпространението на асинхронен контекст се отнася до способността да се поддържа и разпространява контекстна информация (напр. ID на заявки, данни за удостоверяване на потребители, корелационни ID) през асинхронни операции. Без правилно разпространение на контекста става трудно да се проследяват заявки, да се съотнасят логове и да се диагностицират проблеми с производителността в разпределени системи.
Традиционните подходи за управление на контекст често разчитат на изрично предаване на контекстни обекти чрез извиквания на функции, което може да доведе до многословен и податлив на грешки код. AsyncLocalStorage предлага по-елегантно решение, като предоставя начин за съхраняване и извличане на контекстни данни в рамките на един-единствен контекст на изпълнение, дори и през асинхронни операции.
Представяне на AsyncLocalStorage
AsyncLocalStorage е вграден модул в Node.js (наличен от Node.js v14.5.0), който предоставя начин за съхраняване на данни, локални за жизнения цикъл на дадена асинхронна операция. Той по същество създава пространство за съхранение, което се запазва при await извиквания, обещания (promises) и други асинхронни граници. Това позволява на разработчиците да достъпват и променят контекстни данни, без да ги предават изрично.
Ключови характеристики на AsyncLocalStorage:
- Автоматично разпространение на контекст: Стойностите, съхранени в
AsyncLocalStorage, се разпространяват автоматично през асинхронни операции в рамките на същия контекст на изпълнение. - Опростен код: Намалява нуждата от изрично предаване на контекстни обекти чрез извиквания на функции.
- Подобрена наблюдаемост: Улеснява проследяването на заявки и съотнасянето на логове и метрики.
- Нишкова безопасност (Thread-Safety): Осигурява нишково-безопасен достъп до контекстни данни в рамките на текущия контекст на изпълнение.
Сценарии за употреба на AsyncLocalStorage
AsyncLocalStorage е ценен в различни сценарии, включително:
- Проследяване на заявки: Присвояване на уникален ID на всяка входяща заявка и разпространяването му през целия жизнен цикъл на заявката за целите на проследяването.
- Удостоверяване и оторизация: Съхраняване на данни за удостоверяване на потребителя (напр. потребителско ID, роли, разрешения) за достъп до защитени ресурси.
- Логове и одит: Прикачване на специфични за заявката метаданни към съобщенията в логовете за по-добро отстраняване на грешки и одит.
- Мониторинг на производителността: Проследяване на времето за изпълнение на различни компоненти в рамките на заявка за анализ на производителността.
- Управление на трансакции: Управление на трансакционно състояние през множество асинхронни операции (напр. трансакции в база данни).
Практически пример: Проследяване на заявки с AsyncLocalStorage
Нека илюстрираме как да използваме AsyncLocalStorage за проследяване на заявки в просто Node.js приложение. Ще създадем междинен софтуер (middleware), който присвоява уникален ID на всяка входяща заявка и го прави достъпен през целия жизнен цикъл на заявката.
Примерен код
Първо, инсталирайте необходимите пакети (ако е нужно):
npm install uuid express
Ето и кода:
// app.js
const express = require('express');
const { AsyncLocalStorage } = require('async_hooks');
const { v4: uuidv4 } = require('uuid');
const app = express();
const asyncLocalStorage = new AsyncLocalStorage();
const port = 3000;
// Middleware to assign a request ID and store it in AsyncLocalStorage
app.use((req, res, next) => {
const requestId = uuidv4();
asyncLocalStorage.run(new Map(), () => {
asyncLocalStorage.getStore().set('requestId', requestId);
next();
});
});
// Simulate an asynchronous operation
async function doSomethingAsync() {
return new Promise(resolve => {
setTimeout(() => {
const requestId = asyncLocalStorage.getStore().get('requestId');
console.log(`[Async] Request ID: ${requestId}`);
resolve();
}, 50);
});
}
// Route handler
app.get('/', async (req, res) => {
const requestId = asyncLocalStorage.getStore().get('requestId');
console.log(`[Route] Request ID: ${requestId}`);
await doSomethingAsync();
res.send(`Hello World! Request ID: ${requestId}`);
});
app.listen(port, () => {
console.log(`App listening at http://localhost:${port}`);
});
В този пример:
- Създаваме инстанция на
AsyncLocalStorage. - Дефинираме междинен софтуер, който присвоява уникален ID на всяка входяща заявка, използвайки библиотеката
uuid. - Използваме
asyncLocalStorage.run(), за да изпълним обработчика на заявката в контекста наAsyncLocalStorage. Това гарантира, че всички стойности, съхранени вAsyncLocalStorage, са достъпни през целия жизнен цикъл на заявката. - Вътре в междинния софтуер съхраняваме ID на заявката в
AsyncLocalStorage, използвайкиasyncLocalStorage.getStore().set('requestId', requestId). - Дефинираме асинхронна функция
doSomethingAsync(), която симулира асинхронна операция и извлича ID на заявката отAsyncLocalStorage. - В обработчика на маршрута (route handler) извличаме ID на заявката от
AsyncLocalStorageи го включваме в отговора.
Когато стартирате това приложение и изпратите заявка към http://localhost:3000, ще видите ID на заявката в логовете както от обработчика на маршрута, така и от асинхронната функция, което демонстрира, че контекстът се разпространява правилно.
Обяснение
- Инстанция на
AsyncLocalStorage: Създаваме инстанция наAsyncLocalStorage, която ще съхранява нашите контекстни данни. - Междинен софтуер (Middleware): Междинният софтуер прихваща всяка входяща заявка. Той генерира UUID и след това използва
asyncLocalStorage.run, за да изпълни останалата част от обработката на заявката *в рамките* на контекста на това хранилище. Това е от решаващо значение; то гарантира, че всичко надолу по веригата има достъп до съхранените данни. asyncLocalStorage.run(new Map(), ...): Този метод приема два аргумента: нов, празенMap(можете да използвате и други структури от данни, ако са подходящи за вашия контекст) и callback функция. Callback функцията съдържа кода, който трябва да се изпълни в асинхронния контекст. Всички асинхронни операции, инициирани в рамките на този callback, автоматично ще наследят данните, съхранени вMap.asyncLocalStorage.getStore(): Това връщаMap, който е бил предаден наasyncLocalStorage.run. Използваме го, за да съхраняваме и извличаме ID на заявката. Акоrunне е бил извикан, това ще върнеundefined, поради което е важно да извикатеrunв рамките на междинния софтуер.- Асинхронна функция: Функцията
doSomethingAsyncсимулира асинхронна операция. Важно е, че въпреки че е асинхронна (използвайкиsetTimeout), тя все още има достъп до ID на заявката, защото се изпълнява в контекста, установен отasyncLocalStorage.run.
Разширена употреба: Комбиниране с библиотеки за логове
Интегрирането на AsyncLocalStorage с библиотеки за водене на логове (като Winston или Pino) може значително да подобри наблюдаемостта на вашите приложения. Чрез инжектиране на контекстни данни (напр. ID на заявка, ID на потребител) в съобщенията на логовете, можете лесно да съотнасяте логове и да проследявате заявки в различни компоненти.
Пример с Winston
// logger.js
const winston = require('winston');
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(({ timestamp, level, message }) => {
const requestId = asyncLocalStorage.getStore() ? asyncLocalStorage.getStore().get('requestId') : 'N/A';
return `${timestamp} [${level}] [${requestId}] ${message}`;
})
),
transports: [
new winston.transports.Console()
]
});
module.exports = {
logger,
asyncLocalStorage
};
// app.js (modified)
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const { logger, asyncLocalStorage } = require('./logger');
const app = express();
const port = 3000;
app.use((req, res, next) => {
const requestId = uuidv4();
asyncLocalStorage.run(new Map(), () => {
asyncLocalStorage.getStore().set('requestId', requestId);
logger.info(`Incoming request: ${req.url}`); // Log the incoming request
next();
});
});
async function doSomethingAsync() {
return new Promise(resolve => {
setTimeout(() => {
logger.info('Doing something async...');
resolve();
}, 50);
});
}
app.get('/', async (req, res) => {
logger.info('Handling request...');
await doSomethingAsync();
res.send('Hello World!');
});
app.listen(port, () => {
logger.info(`App listening at http://localhost:${port}`);
});
В този пример:
- Създаваме инстанция на логър на Winston и го конфигурираме да включва ID на заявката от
AsyncLocalStorageвъв всяко съобщение. Ключовата част еwinston.format.printf, който извлича ID на заявката (ако е налично) отAsyncLocalStorage. Проверяваме далиasyncLocalStorage.getStore()съществува, за да избегнем грешки при записване на логове извън контекста на заявка. - Актуализираме междинния софтуер, за да записва в лога URL адреса на входящата заявка.
- Актуализираме обработчика на маршрута и асинхронната функция, за да записват съобщения, използвайки конфигурирания логър.
Сега всички съобщения в логовете ще включват ID на заявката, което улеснява проследяването на заявките и съотнасянето на логовете.
Алтернативни подходи: cls-hooked и Async Hooks
Преди AsyncLocalStorage да стане достъпен, библиотеки като cls-hooked бяха често използвани за разпространение на асинхронен контекст. cls-hooked използва Async Hooks (API на по-ниско ниво в Node.js), за да постигне подобна функционалност. Въпреки че cls-hooked все още се използва широко, AsyncLocalStorage обикновено е предпочитан поради вградената си природа и подобрената производителност.
Async Hooks (async_hooks)
Async Hooks предоставят API на по-ниско ниво за проследяване на жизнения цикъл на асинхронните операции. Въпреки че AsyncLocalStorage е изграден върху Async Hooks, директното използване на Async Hooks често е по-сложно и по-малко производително. Async Hooks са по-подходящи за много специфични, напреднали случаи на употреба, където се изисква фин контрол върху асинхронния жизнен цикъл. Избягвайте да използвате Async Hooks директно, освен ако не е абсолютно необходимо.
Защо да предпочетем AsyncLocalStorage пред cls-hooked?
- Вграден:
AsyncLocalStorageе част от ядрото на Node.js, което елиминира нуждата от външни зависимости. - Производителност:
AsyncLocalStorageобикновено е по-производителен отcls-hookedпоради своята оптимизирана имплементация. - Поддръжка: Като вграден модул,
AsyncLocalStorageсе поддържа активно от основния екип на Node.js.
Съображения и ограничения
Въпреки че AsyncLocalStorage е мощен инструмент, е важно да сте наясно с неговите ограничения:
- Граници на контекста:
AsyncLocalStorageразпространява контекст само в рамките на един и същ контекст на изпълнение. Ако предавате данни между различни процеси или сървъри (напр. чрез опашки за съобщения или gRPC), все пак ще трябва изрично да сериализирате и десериализирате контекстните данни. - Изтичане на памет (Memory Leaks): Неправилната употреба на
AsyncLocalStorageможе потенциално да доведе до изтичане на памет, ако контекстните данни не се почистват правилно. Уверете се, че използватеasyncLocalStorage.run()правилно и избягвайте съхраняването на големи количества данни вAsyncLocalStorage. - Сложност: Въпреки че
AsyncLocalStorageопростява разпространението на контекст, той може също да добави сложност към кода ви, ако не се използва внимателно. Уверете се, че екипът ви разбира как работи и следва най-добрите практики. - Не е заместител на глобалните променливи:
AsyncLocalStorage*не е* заместител на глобалните променливи. Той е специално проектиран за разпространение на контекст в рамките на една заявка или трансакция. Прекомерната му употреба може да доведе до силно свързан код и да затрудни тестването.
Най-добри практики за използване на AsyncLocalStorage
За да използвате ефективно AsyncLocalStorage, вземете предвид следните най-добри практики:
- Използвайте междинен софтуер (Middleware): Използвайте междинен софтуер, за да инициализирате
AsyncLocalStorageи да съхранявате контекстни данни в началото на всяка заявка. - Съхранявайте минимално количество данни: Съхранявайте само съществени контекстни данни в
AsyncLocalStorage, за да сведете до минимум натоварването на паметта. Избягвайте съхраняването на големи обекти или чувствителна информация. - Избягвайте директен достъп: Капсулирайте достъпа до
AsyncLocalStorageзад добре дефинирани API-та, за да избегнете силно свързване и да подобрите поддръжката на кода. Създайте помощни функции или класове за управление на контекстни данни. - Обмислете обработката на грешки: Имплементирайте обработка на грешки, за да се справяте елегантно със случаи, в които
AsyncLocalStorageне е правилно инициализиран. - Тествайте обстойно: Пишете единични и интеграционни тестове, за да се уверите, че разпространението на контекст работи както се очаква.
- Документирайте употребата: Ясно документирайте как се използва
AsyncLocalStorageвъв вашето приложение, за да помогнете на други разработчици да разберат механизма за разпространение на контекст.
Интеграция с OpenTelemetry
OpenTelemetry е рамка за наблюдаемост с отворен код, която предоставя API, SDK и инструменти за събиране и експортиране на телеметрични данни (напр. трасета, метрики, логове). AsyncLocalStorage може да бъде безпроблемно интегриран с OpenTelemetry за автоматично разпространение на контекста на трасето през асинхронни операции.
OpenTelemetry разчита силно на разпространението на контекст за съотнасяне на трасета между различни услуги. Използвайки AsyncLocalStorage, можете да гарантирате, че контекстът на трасето се разпространява правилно във вашето Node.js приложение, което ви позволява да изградите цялостна система за разпределено проследяване.
Много OpenTelemetry SDK-та автоматично използват AsyncLocalStorage (или cls-hooked, ако AsyncLocalStorage не е наличен) за разпространение на контекст. Проверете документацията на избрания от вас OpenTelemetry SDK за конкретни подробности.
Заключение
AsyncLocalStorage е ценен инструмент за управление на разпространението на асинхронен контекст в сървърни JavaScript приложения. Използвайки го за проследяване на заявки, удостоверяване, водене на логове и други случаи на употреба, можете да изградите по-стабилни, наблюдаеми и лесни за поддръжка приложения. Въпреки че съществуват алтернативи като cls-hooked и Async Hooks, AsyncLocalStorage обикновено е предпочитаният избор поради вградената си природа, производителност и лекота на използване. Не забравяйте да следвате най-добрите практики и да сте наясно с неговите ограничения, за да използвате ефективно възможностите му. Способността да се проследяват заявки и да се съотнасят събития през асинхронни операции е от решаващо значение за изграждането на мащабируеми и надеждни системи, особено в микросървисни архитектури и сложни разпределени среди. Използването на AsyncLocalStorage помага за постигането на тази цел, което в крайна сметка води до по-добро отстраняване на грешки, мониторинг на производителността и цялостно здраве на приложението.